KM的博客.

Flutter 核心原理

字数统计: 3.8k阅读时长: 14 min
2021/10/15

Flutter

  • 1.开发效率高- 快发阶段 JIT 即时编译,支持 HotReload,节省开发时间.发布节点 AOT提前编译生成高效机器码保证应用性能
  • 2.高性能-基于 Skia 引擎提供高保证的 UI 体验
  • 3.快速内存分配-Flutter 框架使用函数式流,这使得它在很大程度上依赖于底层的内存分配器。
  • 4.类型安全和空安全-2.12 后开始支持空安全和静态类型检测,在编译前提前发现错误,并排除潜在的问题。

Flutter 技术栈

Flutter的三棵树

Flutter 核心原理

JIT 即时编译与 AOT 提前编译

Flutter 是一个跨平台的 UI 工具包,旨在允许代码在 iOS 和 Android 等操作系统之间重用,同时还允许应用程序直接与底层平台服务交互。目标是使开发人员能够交付在不同平台上感觉自然的高性能应用程序,在共享尽可能多的代码的同时接受它们存在的差异。

  • JIT: 在开发过程中,Flutter 应用程序在虚拟机中运行,该虚拟机提供有状态的热重载更改,而无需完全重新编译。
  • AOT: 对于发布,Flutter 应用程序直接编译为机器代码,无论是 Intel x64 还是 ARM 指令,或者如果针对 Web,则编译为 JavaScript。

Flutter Native架构

Flutter 被设计成一个可扩展的分层系统。它作为一系列独立的库存在,每个库都依赖于底层。任何层都没有特权访问下面的层,并且框架层的每个部分都被设计为可选和可替换的。

  • Framework框架层
    • dart:ui层:包括 Foundation/Animation/Painting/Gestures,这是Flutter engine暴露出来的底层 UI 库,提供了动画、手势和绘制的能力
    • Rending渲染层,渲染层依赖 Dart UI层,负责渲染对象对应的渲染树,当动态更新 Widget 时,渲染树对比变化的部分,更新渲染。渲染层负责确定渲染对象的位置、大小和坐标变换和绘制
    • Widget是基础组件库,提供了 Materia 和 Cupertino 两种视觉风格的组件库
  • Engine引擎层
    • 引擎层包含 Skia 引擎、Dart 运行时。文字排版引擎。调用 dart:ui库最终调用的是引擎层之后进行绘制和显示
  • Embeded嵌入层负责将 Flutter 引擎嵌入到特定的系统中。嵌入层采用当前平台的语言编写.
    • 例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。

Flutter Web架构

一、Flutter渲染和布局

Flutter绕过系统 UI 部件库,转而使用自己的小部件集。绘制 Flutter 视觉效果的 Dart 代码被编译成原生代码,使用 Skia 进行渲染。Flutter 还嵌入了自己的 Skia 副本作为引擎的一部分,即使手机没有更新新的 Android 版本,开发人员也可以升级他们的应用程序以保持最新的性能改进。Flutter 在其他原生平台上也是如此,例如 iOS、Windows 或 macOS。

从用户输入到 GPU

Flutter 应用于其渲染管道的最重要原则是 简单即快速。Flutter 对数据如何流向系统有一个简单的管道,如下面的时序图所示:

渲染管线时序图

布局和渲染


所有RenderObjects 的根是RenderView,它表示渲染树的总输出。当平台要求渲染新帧时(例如,由于 vsync或由于纹理解压缩/上传完成),将调用该 compositeFrame()方法,该方法是RenderView渲染树根部对象的一部分. 这将创建一个SceneBuilder来触发场景的更新。场景完成后,RenderView对象将合成的场景传递给 中的Window.render()方法dart:ui,该方法将控制权传递给 GPU 进行渲染。

平台渠道

对于移动和桌面应用程序,Flutter 允许您通过_平台通道_调用自定义代码,这是一种用于在您的 Dart 代码和宿主应用程序的平台特定代码之间进行通信的机制。通过创建公共通道(封装名称和编解码器),您可以在 Dart 和使用 Kotlin 或 Swift 等语言编写的平台组件之间发送和接收消息。数据从 Dart 类型序列Map化为标准格式,然后反序列化为 Kotlin(如 HashMap)或 Swift(如Dictionary)中的等效表示。

平台通道如何让 Flutter 与宿主代码通信

Flutter三棵树与渲染流程

三棵树

  • Widget树
  • Element 树 - Element创建相应的RenderObject并关联到Element.renderObject属性上
  • RenderObject 树-RenderObject的主要职责是Layout和绘制,所有的RenderObject会组成一棵渲染树Render Tree。

    我们重点看一下Element,根据Widget生成的Element创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

Element的生命周期如下:

  1. Framework 调用Widget.createElement 创建一个Element实例,记为element
  2. Framework 调用 element.mount(parentElement,newSlot) ,mount方法中首先调用element所对应Widget的createRenderObject方法创建与element相关联的RenderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在Element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了(可以隐藏)。
  3. 当有父Widget的配置数据改变时,同时其State.build返回的Widget结构与之前不同,此时就需要重新构建对应的Element树。为了进行Element复用,在Element重新构建前会先尝试是否可以复用旧树上相同位置的element,element节点在更新前都会调用其对应Widget的canUpdate方法,如果返回true,则复用旧Element,旧的Element会使用新Widget配置数据更新,反之则会创建一个新的Element。Widget.canUpdate主要是判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就会返回false。根据这个原理,当我们需要强制更新一个Widget时,可以通过指定不同的Key来避免复用。
  4. 当有祖先Element决定要移除element 时(如Widget树结构发生了变化,导致element对应的Widget被移除),这时该祖先Element就会调用deactivateChild 方法来移除它,移除后element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate 方法,这时element状态变为“inactive”状态。
  5. “inactive”态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,Framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
  6. 如果element要重新插入到Element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么Framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。

RenderObject就是渲染树中的一个对象,它主要的作用是实现事件响应以及渲染管线中除过 build 的部分(build 部分由 element 实现),即包括:布局、绘制、层合成以及上屏,这些我们将在后面章节介绍。

RenderObject拥有一个parent和一个parentData 属性,parent指向渲染树中自己的父节点,而parentData是一个预留变量,在父组件的布局过程,会确定其所有子组件布局信息(如位置信息,即相对于父组件的偏移),而这些布局信息需要在布局阶段保存起来,因为布局信息在后续的绘制阶段还需要被使用(用于确定组件的绘制位置),而parentData属性的主要作用就是保存布局信息,比如在 Stack 布局中,RenderStack就会将子元素的偏移数据存储在子元素的parentData中(具体可以查看Positioned实现)。

RenderObject类本身实现了一套基础的布局和绘制协议,但是并没有定义子节点模型(如一个节点可以有几个子节点,没有子节点?一个?两个?或者更多?)。 它也没有定义坐标系统(如子节点定位是在笛卡尔坐标中还是极坐标?)和具体的布局协议(是通过宽高还是通过constraint和size?,或者是否由父节点在子节点布局之前或之后设置子节点的大小和位置等)。

为此,Flutter框架提供了一个RenderBox和一个 RenderSliver类,它们都是继承自RenderObject,布局坐标系统采用笛卡尔坐标系,屏幕的(top, left)是原点。而 Flutter 基于这两个类分别实现了基于 RenderBox 的盒模型布局和基于 Sliver 的按需加载模型,这个我们已经在前面章节介绍过

1.BuildContext是什么?有什么作用?

/// [BuildContext] objects are actually [Element] objects. The [BuildContext]
/// interface is used to discourage direct manipulation of [Element] objects.

BuildContext 是 Element对象,设计BuildContext的目的是避免我们直接操作Element对象。

BuildContext 就是我们 Widget 对应的 Element 对象,我们通过context 在 StatelessWidgetStatefulWidget 中的 build 方法直接访问到 Element 对象。

2. setState 作用是什么?

Notify the framework that the internal state of this object has changed.
Calling [setState] notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a [build] for this [State] object.

通知dart 系统其内部对象的状态已经改变需要刷新
当widget发生改变时,在setState调用markNeedsBuild()方法将当前Element标记为dirty即可,标记为dirty的Element会在下一帧中重建。实际上,State.setState()在内部也是调用的markNeedsBuild()方法。

3 flutter如何原生通信?

我们知道一个完整的Flutter应用程序实际上包括原生代码和Flutter代码两部分。
Flutter 中提供了平台通道(platform channel)用于Flutter和原生平台的通信,平台通道正是Flutter和原生之间通信的桥梁,它也是Flutter插件的底层基础设施。
Flutter与原生之间的通信本质上是一个远程调用(RPC),通过消息传递实现:

  • 应用的Flutter部分通过平台通道(platform channel)将调用消息发送到宿主应用。
  • 宿主监听平台通道,并接收该消息。然后它会调用该平台的API,并将响应发送回Flutter。

二 、Flutter启动流程

Flutter App启动的三个步骤:

  • 1 main()->runApp()->WidgetsFlutterBinding 初始化(ensureInitialized()),与 Flutter Engine 进行通信。

  • 2 绑定根节点创建flutter 著名的三棵树(scheduleAttachRootWidget(app))进行布局。

  • 3 将布局出来的树进行渲染(scheduleWarmUpFrame()

WidgetsFlutterBinding 将是 Widget 架构和 Flutter Engine 连接的核心桥梁,也是整个 Flutter 应用层的核心。通过 ensureInitialized() 方法我们可以得到一个全局单例 WidgetsFlutterBinding。

Flutter的入口在”lib/main.dart”的main()函数中,它是Dart应用程序的起点。在Flutter应用中,main()函数最简单的实现如下:

1
void main() => runApp(MyApp());

可以看main()函数只调用了一个runApp()方法,我们看看runApp()方法中都做了什么:

1
2
3
4
5
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}

参数app是一个 widget,它是 Flutter 应用启动后要展示的第一个组件。

WidgetsFlutterBinding正是绑定Framework 框架和Flutter 引擎的桥梁,定义如下:

我们打印下执行顺序,如下图示:

img

通过查看这些 Binding的源码,我们可以发现WidgetsFlutterBinding通过这些Binding中来监听并处理Window对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。

  • GestureBinding:提供了window.onPointerDataPacket 回调,绑定Framework手势子系统,是Framework事件模型与底层事件的绑定入口。
  • ServicesBinding:提供了window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和Flutter通信。
  • SchedulerBinding:提供了window.onBeginFramewindow.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统。
  • PaintingBinding:绑定绘制库,主要用于处理图片缓存。
  • SemanticsBinding:语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持。
  • RendererBinding: 提供了window.onMetricsChangedwindow.onTextScaleFactorChanged 等回调。它是渲染树与Flutter engine的桥梁。
  • WidgetsBinding:提供了window.onLocaleChangedonBuildScheduled 等回调。它是Flutter widget层与engine的桥梁。

WidgetsFlutterBinding作用

WidgetsFlutterBinding.ensureInitialized()负责初始化一个WidgetsBinding的全局单例,紧接着会调用WidgetsBindingattachRootWidget方法,该方法负责将根Widget添加到RenderView,代码如下:

1
2
3
4
5
6
7
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}

注意,代码中的有renderViewrenderViewElement两个变量,**renderView是一个RenderObject,它是渲染树的根,而renderViewElementrenderView对应的Element对象,可见该方法主要完成了根widget到根 RenderObject再到根Element的整个关联过程。**

组件树在构建(build)完毕后,回到runApp的实现中,当调用完attachRootWidget后,最后一行会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法,该方法的实现在SchedulerBinding 中,它被调用后会立即进行一次绘制,在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。

三、Flutter列表优化

关键词: WidgetsFlutterBindinghandleBeginFramehandleDrawFrame

优化方式

  • 局部刷新
  • 高度使用CacheExtends缓存
  • 推荐使用 Selector Consumer 避免不必要的 diff 计算和布局计算
  • 减少使用saveLayer/使用图片替换半透明效果以减轻 Raster 线程的开销
  • DSL 解析放到 isolate 中将最大限度降低开销

列表空间滑动帧渲染流程

局部刷新

UI 层
  • setState 刷新最小化原则
  • 高度已知的情况下使用 itemExtent
  • 使用 Selector 或者 Consumer 来获取祖先 model
  • 避免动画中裁剪图像,创建 Widget
GPU
  • 使用 RepaintBoundary 隔离频繁刷新视图
  • 减少 saveLayer、clipRect、clipRRect 的使用
  • 使用 AnimatedOpacity 后者 FadeInImage 替换 OpacityWidget
  • 使用图片替换半透明效果
  • 避免使用带有换行符的长文本

参考:

CATALOG
  1. 1. Flutter
    1. 1.1. Flutter 技术栈
    2. 1.2. Flutter的三棵树
  2. 2. Flutter 核心原理
    1. 2.1. JIT 即时编译与 AOT 提前编译
      1. 2.1.1. Flutter Native架构
      2. 2.1.2. Flutter Web架构
    2. 2.2. 一、Flutter渲染和布局
      1. 2.2.1. 从用户输入到 GPU
      2. 2.2.2. 布局和渲染
      3. 2.2.3. 平台渠道
      4. 2.2.4. Flutter三棵树与渲染流程
        1. 2.2.4.0.1. 三棵树
      5. 2.2.4.1. Element的生命周期如下:
      6. 2.2.4.2. 1.BuildContext是什么?有什么作用?
      7. 2.2.4.3. 2. setState 作用是什么?
      8. 2.2.4.4. 3 flutter如何原生通信?
  3. 2.3. 二 、Flutter启动流程
    1. 2.3.1. Flutter App启动的三个步骤:
    2. 2.3.2. WidgetsFlutterBinding作用
  4. 2.4. 三、Flutter列表优化
    1. 2.4.1. 优化方式
      1. 2.4.1.1. 列表空间滑动帧渲染流程
      2. 2.4.1.2. 局部刷新
        1. 2.4.1.2.1. UI 层
        2. 2.4.1.2.2. GPU
      3. 2.4.1.3. 参考: